// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:typed_data';

import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/src/dart/element/element.dart';
import 'package:analyzer/src/generated/source.dart';
import 'package:analyzer/src/summary2/bundle_reader.dart';
import 'package:analyzer/src/summary2/data_reader.dart';
import 'package:analyzer/src/summary2/data_writer.dart';
import 'package:analyzer/src/summary2/linked_element_factory.dart';
import 'package:analyzer/src/util/collection.dart';
import 'package:collection/collection.dart';
import 'package:snooperlang/src/snooperParser.dart';

Uint8List writeUnitInformative(CompilationUnit unit) {
  var byteSink = ByteSink();
  var sink = BufferedSink(byteSink);
  _InformativeDataWriter(sink).write(unit);
  return sink.flushAndTake();
}

/// We want to have actual offsets for tokens of various constants in the
/// element model, such as metadata and constant initializers. But we read
/// these additional pieces of resolution data later, on demand. So, these
/// offsets are different from `nameOffset` for example, which are applied
/// directly after creating corresponding elements during a library loading.
class ApplyConstantOffsets {
  Uint32List? _offsets;
  void Function(_OffsetsApplier)? _function;

  ApplyConstantOffsets(this._offsets, this._function);

  void perform() {
    var offsets = _offsets;
    var function = _function;
    if (offsets != null && function != null) {
      var applier = _OffsetsApplier(
        _SafeListIterator(offsets),
      );
      function.call(applier);
      // Clear the references to possible closure data.
      // TODO(scheglov) We want to null the whole `linkedData` instead.
      _offsets = null;
      _function = null;
    }
  }
}

class InformativeDataApplier {
  final LinkedElementFactory _elementFactory;
  final Map<Uri, Uint8List> _unitsInformativeBytes2;

  InformativeDataApplier(
    this._elementFactory,
    this._unitsInformativeBytes2,
  );

  void applyTo(LibraryElementImpl libraryElement) {
    if (_elementFactory.isApplyingInformativeData) {
      throw StateError('Unexpected recursion.');
    }
    _elementFactory.isApplyingInformativeData = true;

    var unitElements = libraryElement.units;
    for (var i = 0; i < unitElements.length; i++) {
      var unitElement = unitElements[i] as CompilationUnitElementImpl;
      var unitUri = unitElement.source.uri;
      var unitInfoBytes = _unitsInformativeBytes2[unitUri];
      if (unitInfoBytes != null) {
        var unitReader = SummaryDataReader(unitInfoBytes);
        var unitInfo = _InfoUnit(unitReader);

        if (i == 0) {
          _applyToLibrary(libraryElement, unitInfo);
        }

        unitElement.setCodeRange(unitInfo.codeOffset, unitInfo.codeLength);
        unitElement.lineInfo = LineInfo(unitInfo.lineStarts);

        _applyToAccessors(unitElement.accessors, unitInfo.accessors);


        forCorrespondingPairs(unitElement.functions, unitInfo.functions,
            _applyToFunctionDeclaration);


        forCorrespondingPairs(unitElement.topLevelVariables,
            unitInfo.topLevelVariable, _applyToTopLevelVariable);

      }
    }

    _elementFactory.isApplyingInformativeData = false;
  }

  void _applyToAccessors(
    List<PropertyAccessorElement> elementList,
    List<_InfoMethodDeclaration> infoList,
  ) {
    forCorrespondingPairs<PropertyAccessorElement, _InfoMethodDeclaration>(
      elementList.notSynthetic,
      infoList,
      (element, info) {
        element as PropertyAccessorElementImpl;
        element.setCodeRange(info.codeOffset, info.codeLength);
        element.nameOffset = info.nameOffset;
        element.documentationComment = info.documentationComment;
        _applyToFormalParameters(
          element.parameters_unresolved,
          info.parameters,
        );

        var linkedData = element.linkedData;
        if (linkedData is PropertyAccessorElementLinkedData) {
          linkedData.applyConstantOffsets = ApplyConstantOffsets(
            info.constantOffsets,
            (applier) {
              applier.applyToMetadata(element);
              applier.applyToTypeParameters(element.typeParameters);
              applier.applyToFormalParameters(element.parameters);
            },
          );
        }
      },
    );
  }

  void _applyToFormalParameters(
    List<ParameterElement> parameters,
    List<_InfoFormalParameter> infoList,
  ) {
    forCorrespondingPairs<ParameterElement, _InfoFormalParameter>(
      parameters,
      infoList,
      (element, info) {
        element as ParameterElementImpl;
        element.setCodeRange(info.codeOffset, info.codeLength);
        element.nameOffset = info.nameOffset;
        _applyToTypeParameters(element.typeParameters, info.typeParameters);
        _applyToFormalParameters(element.parameters, info.parameters);
      },
    );
  }

  void _applyToFunctionDeclaration(
    FunctionElement element,
    _InfoFunctionDeclaration info,
  ) {
    element as FunctionElementImpl;
    element.setCodeRange(info.codeOffset, info.codeLength);
    element.nameOffset = info.nameOffset;
    element.documentationComment = info.documentationComment;
    _applyToTypeParameters(
      element.typeParameters_unresolved,
      info.typeParameters,
    );
    _applyToFormalParameters(
      element.parameters_unresolved,
      info.parameters,
    );

    var linkedData = element.linkedData as FunctionElementLinkedData;
    linkedData.applyConstantOffsets = ApplyConstantOffsets(
      info.constantOffsets,
      (applier) {
        applier.applyToMetadata(element);
        applier.applyToFormalParameters(element.parameters);
      },
    );
  }


  void _applyToLibrary(LibraryElementImpl element, _InfoUnit info) {
    element.nameOffset = info.libraryName.offset;
    element.nameLength = info.libraryName.length;

    if (info.docComment.isNotEmpty) {
      element.documentationComment = info.docComment;
    }

    forCorrespondingPairs<ImportElement, _InfoImport>(
      element.imports_unresolved,
      info.imports,
      (element, info) {
        element as ImportElementImpl;
        element.nameOffset = info.nameOffset;

        var prefix = element.prefix;
        if (prefix is PrefixElementImpl) {
          prefix.nameOffset = info.prefixOffset;
        }

        _applyToCombinators(element.combinators, info.combinators);
      },
    );

    forCorrespondingPairs<ExportElement, _InfoExport>(
      element.exports_unresolved,
      info.exports,
      (element, info) {
        element as ExportElementImpl;
        element.nameOffset = info.nameOffset;
        _applyToCombinators(element.combinators, info.combinators);
      },
    );

    forCorrespondingPairs<CompilationUnitElement, _InfoPart>(
      element.parts,
      info.parts,
      (element, info) {
        element as CompilationUnitElementImpl;
        var linkedData = element.linkedData as CompilationUnitElementLinkedData;
        linkedData.applyConstantOffsets = ApplyConstantOffsets(
          info.constantOffsets,
          (applier) {
            applier.applyToMetadata(element);
          },
        );
      },
    );

    var linkedData = element.linkedData as LibraryElementLinkedData;
    linkedData.applyConstantOffsets = ApplyConstantOffsets(
      info.libraryConstantOffsets,
      (applier) {
        applier.applyToMetadata(element);
        applier.applyToDirectives(element.imports);
        applier.applyToDirectives(element.exports);
      },
    );
  }


  void _applyToTopLevelVariable(
    TopLevelVariableElement element,
    _InfoTopLevelVariable info,
  ) {
    element as TopLevelVariableElementImpl;
    element.setCodeRange(info.codeOffset, info.codeLength);
    element.nameOffset = info.nameOffset;
    element.documentationComment = info.documentationComment;

    var linkedData = element.linkedData as TopLevelVariableElementLinkedData;
    linkedData.applyConstantOffsets = ApplyConstantOffsets(
      info.constantOffsets,
      (applier) {
        applier.applyToMetadata(element);
        applier.applyToConstantInitializer(element);
      },
    );
  }

}








class _InfoFormalParameter {
  final int codeOffset;
  final int codeLength;
  final int nameOffset;
  final List<_InfoFormalParameter> parameters;

  factory _InfoFormalParameter(SummaryDataReader reader) {
    return _InfoFormalParameter._(
      codeOffset: reader.readUInt30(),
      codeLength: reader.readUInt30(),
      nameOffset: reader.readUInt30() - 1,
      parameters: reader.readTypedList(
        () => _InfoFormalParameter(reader),
      ),
    );
  }

  _InfoFormalParameter._({
    required this.codeOffset,
    required this.codeLength,
    required this.nameOffset,
    required this.parameters,
  });
}

class _InfoFunctionDeclaration {
  final int codeOffset;
  final int codeLength;
  final int nameOffset;
  final String? documentationComment;
  final List<_InfoFormalParameter> parameters;
  final Uint32List constantOffsets;

  factory _InfoFunctionDeclaration(SummaryDataReader reader) {
    return _InfoFunctionDeclaration._(
      codeOffset: reader.readUInt30(),
      codeLength: reader.readUInt30(),
      nameOffset: reader.readUInt30(),
      documentationComment: reader.readStringUtf8().nullIfEmpty,
      parameters: reader.readTypedList(
        () => _InfoFormalParameter(reader),
      ),
      constantOffsets: reader.readUInt30List(),
    );
  }

  _InfoFunctionDeclaration._({
    required this.codeOffset,
    required this.codeLength,
    required this.nameOffset,
    required this.documentationComment,
    required this.parameters,
    required this.constantOffsets,
  });
}



class _InfoImport {
  final int nameOffset;
  final int prefixOffset;

  factory _InfoImport(SummaryDataReader reader) {
    return _InfoImport._(
      nameOffset: reader.readUInt30(),
      prefixOffset: reader.readUInt30() - 1,

    );
  }

  _InfoImport._({
    required this.nameOffset,
    required this.prefixOffset,
  });
}

class _InfoLibraryName {
  final int offset;
  final int length;

  factory _InfoLibraryName(SummaryDataReader reader) {
    return _InfoLibraryName._(
      offset: reader.readUInt30() - 1,
      length: reader.readUInt30(),
    );
  }

  _InfoLibraryName._({
    required this.offset,
    required this.length,
  });
}



class _InformativeDataWriter {
  final BufferedSink sink;

  _InformativeDataWriter(this.sink);

  void write(CompilationUnit unit) {
    sink.writeUInt30(unit.offset);
    sink.writeUInt30(unit.length);

    sink.writeUint30List(unit.lineInfo?.lineStarts ?? [0]);

    _writeLibraryName(unit);

    sink.writeList2<ImportDirective>(unit.directives, (directive) {
      sink.writeUInt30(directive.keyword.offset);
      sink.writeUInt30(1 + (directive.prefix?.offset ?? -1));

    });


    sink.writeList2<FunctionDecl>(
        unit.declarations
            .whereType<FunctionDecl>()
            .toList(), (node) {
      sink.writeUInt30(node.offset);
      sink.writeUInt30(node.length);
      sink.writeUInt30(node.name.offset);
      _writeFormalParameters(node.functionExpression.parameters);
      _writeOffsets(
        formalParameters: node.functionExpression.parameters,
      );
    });



    sink.writeList<VariableStmt>(
      unit.nodes
          .whereType<VariableStmt>()
          .expand((declaration) => declaration.variables.variables)
          .toList(),
      _writeTopLevelVariable,
    );
  }

  int _codeOffsetForVariable(VariableStmt node) {
    var codeOffset = node.getLeftIToken().getStartOffset();
    var variableList = node.parent as VariableDeclarationList;
    if (variableList.variables[0] == node) {
      codeOffset = variableList.parent!.offset;
    }
    return codeOffset;
  }

  void _writeFormalParameters(FunctionParameterList? parameterList) {
    var parameters = parameterList?.parameters ?? <FormalParameter>[];
    sink.writeList<FormalParameter>(parameters, (node) {
      sink.writeUInt30(node.offset);
      sink.writeUInt30(node.length);
      sink.writeUInt30(1 + (node.identifier?.offset ?? -1));

      var notDefault = node.notDefault;
      if (notDefault is FieldFormalParameter) {
        _writeTypeParameters(notDefault.typeParameters);
        _writeFormalParameters(notDefault.parameters);
      } else if (notDefault is FunctionTypedFormalParameter) {
        _writeTypeParameters(notDefault.typeParameters);
        _writeFormalParameters(notDefault.parameters);
      } else if (notDefault is SuperFormalParameter) {
        _writeTypeParameters(notDefault.typeParameters);
        _writeFormalParameters(notDefault.parameters);
      } else {
        _writeTypeParameters(null);
        _writeFormalParameters(null);
      }
    });
  }


  void _writeLibraryName(CompilationUnit unit) {
    Directive? firstDirective;
    var nameOffset = -1;
    var nameLength = 0;

    sink.writeUInt30(1 + nameOffset);
    sink.writeUInt30(nameLength);
    _writeOffsets(
      importDirectives: unit.directives.whereType<ImportDirective>(),
    );
  }


  void _writeOffsets({
    Iterable<ImportDirective>? importDirectives,
    FormalParameterList? formalParameters,
    Expression? constantInitializer,
  }) {
    var collector = _OffsetsCollector();

    void addDirectives(Iterable<Directive>? directives) {
      if (directives != null) {
        for (var directive in directives) {
          directive.metadata.accept(collector);
        }
      }
    }

    void addTypeParameters(TypeParameterList? typeParameters) {
      if (typeParameters != null) {
        for (var typeParameter in typeParameters.typeParameters) {
          typeParameter.metadata.accept(collector);
        }
      }
    }

    void addFormalParameters(FormalParameterList? formalParameters) {
      if (formalParameters != null) {
        for (var parameter in formalParameters.parameters) {
          parameter.metadata.accept(collector);
          addFormalParameters(
            parameter is FunctionTypedFormalParameter
                ? parameter.parameters
                : null,
          );
          if (parameter is DefaultFormalParameter) {
            parameter.defaultValue?.accept(collector);
          }
        }
      }
    }

    metadata?.accept(collector);
    addDirectives(importDirectives);
    addDirectives(exportDirectives);
    addTypeParameters(typeParameters);
    addFormalParameters(formalParameters);
    constantInitializer?.accept(collector);
    constructorInitializers?.accept(collector);
    if (enumConstants != null) {
      for (var enumConstant in enumConstants) {
        enumConstant.metadata.accept(collector);
      }
    }
    if (aliasedType is GenericFunctionType) {
      addTypeParameters(aliasedType.typeParameters);
      addFormalParameters(aliasedType.parameters);
    }
    sink.writeUint30List(collector.offsets);
  }

  void _writeTopLevelVariable(VariableDeclaration node) {
    var codeOffset = _codeOffsetForVariable(node);
    sink.writeUInt30(codeOffset);
    sink.writeUInt30(node.end - codeOffset);
    sink.writeUInt30(node.name.offset);
    _writeDocumentationComment(node);

    // TODO(scheglov) Replace with some kind of double-iterating list.
    var declaration = node.parent!.parent as TopLevelVariableDeclaration;

    _writeOffsets(
      metadata: declaration.metadata,
      constantInitializer: node.initializer,
    );
  }

  void _writeTypeParameters(TypeParameterList? parameterList) {
    var parameters = parameterList?.typeParameters ?? <TypeParameter>[];
    sink.writeList<TypeParameter>(parameters, (node) {
      sink.writeUInt30(node.offset);
      sink.writeUInt30(node.length);
      sink.writeUInt30(node.name.offset);
    });
  }
}

class _InfoTopLevelVariable {
  final int codeOffset;
  final int codeLength;
  final int nameOffset;
  final String? documentationComment;
  final Uint32List constantOffsets;

  factory _InfoTopLevelVariable(SummaryDataReader reader) {
    return _InfoTopLevelVariable._(
      codeOffset: reader.readUInt30(),
      codeLength: reader.readUInt30(),
      nameOffset: reader.readUInt30(),
      documentationComment: reader.readStringUtf8().nullIfEmpty,
      constantOffsets: reader.readUInt30List(),
    );
  }

  _InfoTopLevelVariable._({
    required this.codeOffset,
    required this.codeLength,
    required this.nameOffset,
    required this.documentationComment,
    required this.constantOffsets,
  });
}


class _InfoUnit {
  final int codeOffset;
  final int codeLength;
  final List<int> lineStarts;
  final _InfoLibraryName libraryName;
  final Uint32List libraryConstantOffsets;
  final String docComment;
  final List<_InfoImport> imports;
  final List<_InfoFunctionDeclaration> functions;
  final List<_InfoTopLevelVariable> topLevelVariable;

  factory _InfoUnit(SummaryDataReader reader) {
    return _InfoUnit._(
      codeOffset: reader.readUInt30(),
      codeLength: reader.readUInt30(),
      lineStarts: reader.readUInt30List(),
      libraryName: _InfoLibraryName(reader),
      libraryConstantOffsets: reader.readUInt30List(),
      docComment: reader.readStringUtf8(),
      imports: reader.readTypedList(
        () => _InfoImport(reader),
      ),

      functions: reader.readTypedList(
        () => _InfoFunctionDeclaration(reader),
      ),

      topLevelVariable: reader.readTypedList(
        () => _InfoTopLevelVariable(reader),
      ),
    );
  }

  _InfoUnit._({
    required this.codeOffset,
    required this.codeLength,
    required this.lineStarts,
    required this.libraryName,
    required this.libraryConstantOffsets,
    required this.docComment,
    required this.imports,

    required this.functions,
    required this.topLevelVariable,
  });
}



class _SafeListIterator<T> {
  final List<T> _elements;
  int _index = 0;

  _SafeListIterator(this._elements);

  bool get hasNext {
    return _index < _elements.length;
  }

  T? take() {
    if (hasNext) {
      return _elements[_index++];
    } else {
      return null;
    }
  }
}

extension on String {
  String? get nullIfEmpty {
    return isNotEmpty ? this : null;
  }
}

extension _ListOfElement<T extends Element> on List<T> {
  List<T> get notSynthetic {
    return where((e) => !e.isSynthetic).toList();
  }
}
